﻿#region MIT Licence
/*
 * Copyright (C) 2014 PdfCreative
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy of this software 
 * and associated documentation files (the "Software"), to deal in the Software without restriction, 
 * including without limitation the rights to use, copy, modify, merge, publish, distribute, 
 * sublicense, and/or sell copies of the Software, and to permit persons to whom 
 * the Software is furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included 
 * in all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 
 * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE 
 * AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 
 * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 */
#endregion

using System;
using System.Collections.Generic;
using System.IO;
using System.Diagnostics;
using System.Text;

using PdfCreative.Pdf.Writer.Utilities;

namespace PdfCreative.Pdf.Writer.Embedding
{
    /// <summary>
    /// The PdfEmbedder class is the main interface for embedding existing PDF documents into a target PDF document.
    /// It parses an existing PDF and embeds the objects into the target document. The PdfEmbedder is an internal
    /// class and not meant to be used on its own.
    /// </summary>
    internal class PdfEmbedder : IPdfValueWriter
    {
        // Constants
        // =========
        #region Constants
        private const int BUFFER_SIZE = 0x20000; // 128KB
        #endregion

        // Private fields
        // ==============
        #region Private fields
        private byte[] _buffer;
        private PdfObjectReference _containingPageTreeRoot;
        private List<CrossReference> _crossReferences;
        private Dictionary<PdfObjectReference, PdfObjectReference> _mappings;
        private int _pageCount = 0;
        private PdfObjectReference _pageTree;
        private PdfParser _parser;
        private StringBuilder _prefix;
        private Stream _out;
        private StreamWriter _writer;
        #endregion



        // Public events
        // =============
        #region Public events
        /// <summary>
        /// The ObjectReferenceRequest event is raised when the PdfEmbedder needs the Writer to 
        /// supply a number of object references.
        /// </summary>
        public event ObjectReferenceCallback ObjectReferenceRequest;
        #endregion



        #region IPdfValueWriter Members
        /// <summary>
        /// Writes the specified dictionary to the output stream.
        /// </summary>
        /// <param name="dictionary">The dictionary</param>
        public void WriteDictionary(PdfDictionary dictionary)
        {
            Debug.Assert(null != dictionary);

            // An empty dictionary is still output, but the open and close tokens need to be on one line in order for PDF readers to
            // correctly interpret the empty dictionary.
            if (dictionary.Count > 0)
            {
                _writer.Write("<<");
                _prefix.Append('\t');
                foreach (KeyValuePair<string, PdfValue> pair in dictionary.GetEntries())
                {
                    _writer.Write("\r\n{0}{1}\t\t", _prefix, pair.Key);
                    pair.Value.Write(this);
                }
                _prefix.Remove(_prefix.Length - 1, 1);
                _writer.Write("\r\n{0}>>", _prefix);
            }
            else _writer.WriteLine("<<>>");
        }

        /// <summary>
        /// Writes the specified list to the output stream.
        /// </summary>
        /// <param name="list">The list</param>
        public void WriteList(PdfList list)
        {
            _writer.Write("[");
            foreach (PdfValue value in list)
            {
                _writer.Write(" ");
                value.Write(this);
            }
            _writer.Write("]");
        }

        /// <summary>
        /// Writes the specified object reference to the output stream.
        /// </summary>
        /// <param name="obj">The object reference</param>
        public void WriteObjectReference(PdfObjectReference obj)
        {
            _writer.Write(ConvertObjectReference(obj));
        }

        /// <summary>
        /// Writes the specified string value to the output stream.
        /// </summary>
        /// <param name="value">The string value</param>
        public void WriteString(string value)
        {
            _writer.Write(value);
        }
        #endregion



        // Public properties
        // =================
        #region Public properties
        /// <summary>
        /// Sets or retrieves a reference to the containing document's page-tree's root.
        /// </summary>
        public PdfObjectReference ContainingPageTreeRoot
        {
            get { return _containingPageTreeRoot; }
            set { _containingPageTreeRoot = value; }
        }

        /// <summary>
        /// Retrieves the number of pages in this document.
        /// </summary>
        public int PageCount
        {
            get { return _pageCount; }
        }

        /// <summary>
        /// Retrieves the top-level page-tree for this document.
        /// </summary>
        public PdfObjectReference PageTree
        {
            get { return _pageTree; }
        }
        #endregion



        // Public methods
        // ==============
        #region Public methods
        /// <summary>
        /// Embeds a PDF document into the target document. This method returns a list of cross-references
        /// for the objects that have been embedded.
        /// </summary>
        /// <returns>The cross-references of the objects that have been embedded.</returns>
        /// <exception cref="PdfParserException">An error occurred while parsing the PDF document.</exception>
        public CrossReference[] Embed()
        {
            _crossReferences.Clear();

            // We ask the parser to read the document's header.
            if (true == _parser.ParseDocumentHeader())
            {
                // If that has finished succesfully, we ask the target for new identifiers for
                // each of the objects in the source document's cross-reference list.
                MapObjectReferences();

                // We now ask the parser to parse every object in the document.
                // When we find an object, we output it, making sure to convert all object references
                // to their counterparts in the target document.
                _parser.ParseAllObjects();
            }

            // We are done. We need to flush the writer to make sure all the content is added.
            _writer.Flush();

            // Return the list of target cross-references.
            return _crossReferences.ToArray();

        } // Embed()
        #endregion



        // Event handlers
        // ==============
        #region PdfParser events
        /// <summary>
        /// The parser reports that it has found the beginning of an object declaration.
        /// </summary>
        /// <param name="objectRef">The object reference</param>
        private void Parser_BeginObject(PdfObjectReference objectRef)
        {
        }

        /// <summary>
        /// The parser reports that is has reached the end of an object.
        /// </summary>
        /// <param name="objectRef">The object reference</param>
        private void Parser_EndObject(PdfObjectReference objectRef)
        {
            _writer.WriteLine("endobj\r\n");
        }

        /// <summary>
        /// The parser reports that is has fully parsed the contents of an object.
        /// We convert any object references in the object, and parse it.
        /// </summary>
        /// <param name="obj">The object</param>
        private void Parser_Object(PdfObject obj)
        {
            // Convert the object's references and parse its contents.
            ParseObject(obj);

            // We store the object's position in the target file.
            _writer.Flush();
            _crossReferences.Add(new CrossReference(
                ConvertObjectReference(obj.Identifier), _out.Position));

            // Finally, we write the object values.
            WriteObjectValues(obj);
        }

        /// <summary>
        /// When we find a stream we deal with it. If the stream is a binary stream then
        /// we copy it to the output ourselves. If it's a PDF stream then we ask the parser
        /// to read it for us.
        /// </summary>
        /// <param name="objectRef">The object reference of the object that contains the stream</param>
        private void Parser_Stream(PdfObjectReference objectRef)
        {
            // Start with the stream header.
            _writer.WriteLine("stream");
            _writer.Flush();

            // Read blocks from the parser and write them to the output document,
            // until no more data is left.
            int bytesRead = 0;
            do{
                bytesRead = _parser.ReadStreamChunk(_buffer, 0, _buffer.Length);
                _out.Write(_buffer, 0, bytesRead);
            }
            while(bytesRead > 0);

            // We have finished copying the stream contents.
            _writer.WriteLine("\r\nendstream");

        } // Parser_Stream()
        #endregion



        // Private implementation
        // ======================
        #region Private implementation
        /// <summary>
        /// Converts the specified object reference to its matching reference in the target
        /// document.
        /// </summary>
        /// <param name="reference">The reference</param>
        /// <returns>The matching reference</returns>
        private PdfObjectReference ConvertObjectReference(PdfObjectReference reference)
        {
            PdfObjectReference result;
            _mappings.TryGetValue(reference, out result);

            return result;
        }

        /// <summary>
        /// Maps object references in the parsed document to references in the target document.
        /// This method creates mappings between objects in the two documents, and stores these.
        /// </summary>
        private void MapObjectReferences()
        {
            PdfObjectReference[] newReferences = OnObjectReferenceRequest();

            // Then we create a mapping from old references to new ones.
            _mappings = new Dictionary<PdfObjectReference, PdfObjectReference>();
            for (int mappingIndex = 0; mappingIndex < _parser.CrossReferences.Count; mappingIndex++)
                _mappings[_parser.CrossReferences[mappingIndex].ObjectReference] = newReferences[mappingIndex];
        }

        /// <summary>
        /// Raises the ObjectReferenceRequest event, which asks the target for a number of object references.
        /// </summary>
        /// <returns>The object references.</returns>
        private PdfObjectReference[] OnObjectReferenceRequest()
        {
            PdfObjectReference[] references = null;

            if (null != ObjectReferenceRequest)
            {
                int requestedNumber  = _parser.CrossReferences.Count;

                references = ObjectReferenceRequest(requestedNumber);
                if (null == references || references.Length < requestedNumber)
                    throw new PdfEmbedderException(
                        "Please supply the right number of object references. {0} references were requested.", requestedNumber);
            }
            else
                throw new PdfEmbedderException("No listener to ObjectReferenceRequest callback has been installed.");

            return references;
        } // OnObjectReferenceRequest()

        /// <summary>
        /// Parses the specified object. This will determine if the object is of particular interest.
        /// Interesting objects are a document's root or a page set. These are used to collect information
        /// on the number of pages in the embedded document.
        /// </summary>
        /// <param name="obj">The object</param>
        private void ParseObject(PdfObject obj)
        {
            // If this object is the 'pages' object, and it has no parent, we lik it to the containing
            // document's page tree. We make sure NOT to add an object reference instance in order
            // to avoid conversion later on. Instead, we add the object reference as a string value.
            if ("/Pages" == obj.Dictionary["/Type"])
                if (null == obj.Dictionary["/Parent"])
                    obj.Dictionary.Add("/Parent", _containingPageTreeRoot.ToString(PdfObjectNotation.Reference));

            // Now we check to see if this object is the root catalog of the document.
            // If so, we ask for the top-level pages tree and return this.
            if (obj.Identifier.Equals(_parser.DocumentRoot) && "/Catalog" == obj.Type)
            {
                PdfObjectReference pageTreeObject = obj.Dictionary.GetObjectReference("/Pages");
                if (null!=pageTreeObject) 
                    _pageTree = ConvertObjectReference(pageTreeObject);
            }

            // If this is a page tree, and it's of a higher-order one than any tree we loaded so far, we add its count
            // to the document's page count. At some point we will find the top-level tree which will have a count
            // that equals the total number of pages in the document.
            if ("/Pages" == obj.Type)
            {
                int count = _parser.ReadIndirectInt32(obj.Dictionary.GetValue("/Count"));
                if (count > _pageCount) _pageCount = count;
            }
        } // ParseObject()


        /// <summary>
        /// Sets up the parser.
        /// </summary>
        private void SetupParser()
        {
            _parser.BeginObject += Parser_BeginObject;
            _parser.EndObject += Parser_EndObject;
            _parser.Object += Parser_Object;
            _parser.Stream += Parser_Stream;

        }
        #endregion

        #region Writing object values

        /// <summary>
        /// Writes the specified object's values and dictionary to the output stream.
        /// This also converts the object's identifier and any object references contained within its
        /// values.
        /// </summary>
        /// <param name="obj">The object.</param>
        private void WriteObjectValues(PdfObject obj)
        {
            // Write the identifier.
            _writer.WriteLine(ConvertObjectReference(obj.Identifier).ToString(PdfObjectNotation.Declaration));

            // Write the object's value.
            obj.Value.Write(this);
            
            _writer.WriteLine();
        } // WriteObjectValues()
        #endregion



        // Public constructor
        // ==================
        #region Public constructor
        /// <summary>
        /// Constructs a new PdfParser instance.
        /// </summary>
        /// <param name="fin">The input stream</param>
        /// <param name="fout">The output stream</param>
        public PdfEmbedder(Stream fin, Stream fout)
        {
            if (null == fin) throw new ArgumentNullException("fin");
            if (null == fout) throw new ArgumentNullException("fout");

            _buffer = new byte[BUFFER_SIZE];
            _crossReferences = new List<CrossReference>();
            _out = fout;
            _writer = new StreamWriter(fout);

            _parser = new PdfParser(fin);
            _pageTree = PdfObjectReference.Empty;
            _prefix = new StringBuilder();

            SetupParser();
        }
        #endregion

    } // PdfEmbedder class
}
